package matrices;

public class SparseRow {
	private static IllegalArgumentException columnException =
			new IllegalArgumentException("Column numbers must be greater than -1") ,
			elementTypeException = 
			new IllegalArgumentException("Element type must be consistent throught matrix");
	private final SparseRow header;//pointer to the header element, shouldn't be deleted in the life of the row
	private SparseRow next = null;
	private final int column;
	private MatrixElement value;
	
	/*******************************************************************
	 * CONSTRUCTORS
	 */
	
	/**
	 * Copy constructor
	 */
	public SparseRow(SparseRow origional){
		this.header = this;
		this.column = -1;
		this.value = origional.getValueOfDefaultElement().createNewInstance();
		if(origional.next != null) this.next = new SparseRow(origional.next,this);
	}
	
	private SparseRow(SparseRow origional,SparseRow newHeader){
		this.header = newHeader;
		this.column = origional.column;
		this.value = origional.value.createNewInstance();
		if (origional.next != null) this.next = new SparseRow(origional.next, this.header);
	}
	
	/**
	 * Allows the row to be 'filled' with a non-null matrix element, when looking for the value
	 * of an element that has a column that is not occupied, the row will return the value at
	 * column -1; the default value
	 * @param fill
	 */
	public SparseRow(MatrixElement fill){
		this.header = this;
		this.column = -1;
		this.value = fill.createNewInstance();
	}
	
	/**
	 * privately available constructor, to prevent the use of illegal column numbers
	 * @param column
	 * @param value
	 */
	private SparseRow(int column, MatrixElement value, SparseRow header) throws IllegalArgumentException{
		if(column < 0) throw columnException;
		this.header = header;
		this.column = column;
		this.value = value.createNewInstance();
	}
	
	/*******************************************************************
	 * PUBLICLY AVAILABLE ASSIGNMENT METHODS
	 */
	
	public MatrixElement getRowNorm(){
		return this.getSumOfMultiples(this.getTranspose(), 0);
	}
	
	/**
	 * returns the norm of a row, if used with a row's transpose
	 */
	public MatrixElement getSumOfMultiples(SparseRow[] rows, int multiplierColumn){
		MatrixElement sum = this.getValueOfDefaultElement().zeroValue();
		if(rows.length > (this.getLargestColumnNumber()+1)) throw columnException;
		for(int i = 0 ; i <= this.getLargestColumnNumber() ; i ++){
			MatrixElement temp = this.getElementValueByColumn(i).createNewInstance();
			temp = temp.multiply(temp);

			sum = sum.add(temp);
		}
		return sum;
	}
	
	/**
	 * Multiply every element in the row by a given matrix element
	 */
	public SparseRow multiplyElementsBy(MatrixElement multiplier){
		checkElementType(multiplier);
		if(this != header){//dont multiply the header
			this.value.multiply(multiplier);
		}
		if(this.next != null) this.next.multiplyElementsBy(multiplier);
		return this.header;
	}
	
	/** TODO debug properly, sorting is costly and should only be done when absolutely neccesary.
	 * 
	 * Method which sorts the elements in ascending order by their column numbers, by definition
	 * the header column will be the element with column number -1, as it will be the smallest
	 * @return
	 */
	public SparseRow bubbleSort(){
		for(int i = this.getLargestColumnNumber() ; i >= -1 ; i--){
			for(int j = i ; j >= -1 ; j--){
				this.bubble(j);
			}
		}
		return this;
	}
	
	/**
	 * Allows reassignment of the default value of this row to a new value
	 */
	public void setDefaultValue(MatrixElement defaultElement) throws IllegalArgumentException{
		checkElementType(defaultElement);
		this.header.value = defaultElement;
	}
	
	/**
	 * kills all the elements beyond this one by setting their pointers to null. since the method is
	 * public, it can only be called publicly on the leading element, which will survive the process
	 */
	public void emptyRow(){
		if(next != null) next.emptyRow();
		this.next = null;
	}
	
	/**
	 * allows a series of values to be set simultaneously, to save time
	 * @param columns
	 * @param values
	 * @throws IllegalArgumentException
	 */
	public SparseRow setElementValues(int [] columns, MatrixElement [] values) throws IllegalArgumentException{
		if(columns == null || values == null) throw new IllegalArgumentException("Null arrays passed to constructor");
		if(columns.length != values.length) throw new IllegalArgumentException("Array lengths must match.");
		for(int i = 0 ; i < columns.length ; i++){
			checkElementType(values[i]);
			setElementValue(columns[i],values[i]);
		}
		return this;
	}
	
	/**
	 * Sets a value in the row, overriding any elements that may already be in that column
	 * @param column
	 * @param value
	 */
	public SparseRow setElementValue(int column, MatrixElement value) throws IllegalArgumentException{
		if(column < 0) throw columnException;
		checkElementType(value);
		
		if(this.column == column){
			this.value = value.createNewInstance();
		}else if(this.next != null){
			this.next.setElementValue(column, value);
		}else{
			this.next = new SparseRow(column, value, this.header);
		}
		return this;
	}
	
	/**
	 * Removes an element from the row
	 * @param column the column value of the element to be deleted
	 */
	public void deleteElementPointer(int column){
		if(next.column == column){
			deleteNextEntryInRow();
		}else{
			next.deleteElementPointer(column);
		}
	}
	
	/**
	 * 'glues' two rows together, and calls the 'clean' method
	 * @param p
	 * @return
	 */
	public SparseRow combineWith(SparseRow p){
		SparseRow q = new SparseRow(p);//copy the row to prevent pointer errors
		checkElementType(q.value);
		if(this.next != null){
			this.next.combineWith(q);
		}else{
			this.next = q.header.next;
		}
		clean();
		return this.header;
	}
	
	/*******************************************************************
	 * PUBLICLY AVAILABLE RETRIEVAL METHODS
	 */
	
	//TODO this works but an array is a really messy way of doing things
	public SparseRow [] getTranspose(){
		this.header.clean();
		SparseRow [] transp = new SparseRow[this.getLargestColumnNumber()+1];
		for(int i = 0 ; i < transp.length ; i++){
			transp[i] = new SparseRow(this.getValueOfDefaultElement());
			if(this.getElementByColumn(i) != null) transp[i].setElementValue(0, this.getElementByColumn(i).value);
		}
		return transp;
	}
	
	/**
	 * Returns whether the row is an empty header row
	 * @return
	 */
	public boolean isEmpty(){
		return (this.header.next == null);
	}
	
	/**TODO test this works
	 * Returns the number of elements which are not empty
	 * @return
	 */
	public int getNumberOfNonDefaultElements(){
		int numberOfNonDefaultElements = 0;
		SparseRow pointer = this.header;
		while(pointer.next != null){
			if(!pointer.next.value.isEqualTo(this.getValueOfDefaultElement())) numberOfNonDefaultElements += 1;
			pointer = pointer.next;
		}
		return numberOfNonDefaultElements;
	}
	
	/**
	 * returns the default Element value for this row
	 */
	public MatrixElement getValueOfDefaultElement(){
		return this.header.value;
	}
	
	public boolean isDefaultValueEqualTo(MatrixElement b){
		return (this.header.value.isEqualTo(b));
	}
	
	/**
	 * Allows the retrieval of an element based on its column, will cause problems if the matrix has
	 * not yet been cleaned
	 * @param column
	 * @return
	 */
	public MatrixElement getElementValueByColumn(int column){
		if(this.column == -1) clean();//if the header element, clean the row
		if(this.column == column) return this.value.createNewInstance();
		if(this.next == null) return this.getValueOfDefaultElement();
		return this.next.getElementValueByColumn(column);
	}
	
	/**
	 * Gets the biggest column number in the row
	 * @return
	 */
	public int getLargestColumnNumber(){
		int maxColumn = -1;
		SparseRow pointer = this;
		while(pointer.next != null){
			pointer = pointer.next;
			if(pointer.column > maxColumn) maxColumn = pointer.column;
		}
		return maxColumn;
	}
	
	public String printProperties(){
		String returnStr = "";
		int numberOfElements = 0;
		
		returnStr += this.getClass().getName() + " with " + numberOfElements + " elements. " +
					"Default element: " + this.getValueOfDefaultElement() + ", rowSpan: " + this.getLargestColumnNumber();
		
		return returnStr;
	}
	
	public String printRow(){
		String returnStr = "";
		for(int i = 0; i <= this.getLargestColumnNumber() ; i++)  returnStr += ( "|" + getElementValueByColumn(i) ) ;
		returnStr += "|";
		return returnStr;
	}
	
	public String printRow(int maxColumn){
		String returnStr = "";
		for(int i = 0; i <= maxColumn ; i++)  returnStr += ( "|" + getElementValueByColumn(i) ) ;
		returnStr += "|";
		return returnStr;
	}
	
	/**
	 * publicly called method that returns the represnetation of the row in string form
	 */
	public String toString(){//publicly called for the first element in the chain
		return "[header; "+this.getValueOfDefaultElement()+"]" + this.stringChain();
	}
	
	/*******************************************************************
	 * PRIVATE METHODS
	 */
	
	/**
	 * Checks whether an element type matches the canonical element type for this matrix, and
	 * throws an error if the user attempts to add a conflicting element type
	 */
	private void checkElementType(MatrixElement value) throws IllegalArgumentException{
		if(value.getClass() != header.value.getClass()) throw elementTypeException;
	}
	
	/**
	 * Returns a row object by column
	 * @param column
	 * @return
	 */
	private SparseRow getElementByColumn(int column){
		if(this.column == column) return this;
		if(this.next == null) return next;
		return next.getElementByColumn(column);
	}
	
	/**
	 * deletes the next entry in the row
	 */
	private void deleteNextEntryInRow(){
		if(this.next != null){
			this.next = next.next;
		}
	}
	
	/**TODO test this works
	 * Removes duplicate column entries by adding their Elements
	 */
	private void clean(){
		if(this.next != null){
			SparseRow pointerParent = this;
			SparseRow pointer = this.next;
			do{
				if(this.column == pointer.column){
					this.value.add(pointer.value);
					//kill pointer
					pointerParent.next = pointer.next;
				}
				
				if(pointer.value.isEqualTo(this.getValueOfDefaultElement()))
					pointerParent.next = pointer.next;
				
				pointerParent = pointer;
				
				pointer = pointer.next;
				
			}while(pointer != null);
			if(this.next != null)this.next.clean();
		}
	}
	
	private SparseRow bubble(int column){
		if(this.column == column){
			if(this.next != null){
				if(this.next.next != null){
					if(this.next.column > this.next.next.column){
						swapFollowingTwoElements(this.column);
					}
				}
			}
		}else if(next != null){
			next.bubble(column);
		}
		return this.header;
	}
	
	/**
	 * A recursively called method that returns a string of all the values beyond this element in
	 * the row. when called from the header element, it will return a list of all the elements based on
	 * the position of their pointers
	 * @return
	 */
	private String stringChain(){//recursively called
		return this.stringValue() + ((next != null)? "->"+ next.stringChain() : ""); 
	}
	
	/**
	 * Returns the string value of the current element
	 * @return
	 */
	private String stringValue(){
		String returnStr = (this.column>-1)?("[" + this.column + "; " + this.value + "]"):"";
		//System.out.println("\t\tstring value called returning \""+returnStr+"\"");
		return returnStr;
	}
	
	/*******************************************************************
	 * MISC. //TODO
	 */
	
	/**
	 *TODO make this neater it sucks D:
	 * Swaps two elements in the row, based on the column number of the leading element, and 
	 * the pointers of the two next elements
	 * @param column
	 * @return
	 */
	public SparseRow swapFollowingTwoElements(int column){//swaps the next element with the one below that
		if(this.next != null){
			if(this.next.next != null){
				if(this.column == column){
					SparseRow a = this.next;
					SparseRow b = this.next.next;
					SparseRow c = this.next.next.next;
					this.next.next = null;
					b.next = a;
					this.next = b;
					this.next.next.next = c;
				}else{
					next.swapFollowingTwoElements(column);
				}
			}
		}
		return this;
	}
	
	/**
	 * test method, delete when confident it all works (maybe never!)
	 * @param args
	 */
	public static void main(String [] args){
		//create a test row:
		SparseRow myRow = new SparseRow(TestElement.zeroVal());
		System.out.println("Newly instansiated row: " + myRow);
		myRow.setElementValue(10000, new TestElement(10f) );
		myRow.setElementValue(25, new TestElement(2.5f) );
		System.out.println("row with added values: " + myRow);
		myRow.setElementValues( new int[]{  25 , 2, 3 , 4, 5 }, new TestElement[]{
				new TestElement(1),
				new TestElement(2),
				new TestElement(3),
				new TestElement(4),
				new TestElement(5)
		});
		System.out.println( myRow);
		
		int delCol = 3;
		System.out.println("Deleting column: " + delCol);
		myRow.deleteElementPointer(delCol);
		System.out.println(myRow);
		System.out.println("Emptying row");
		myRow.emptyRow();
		System.out.println("Row A: "+myRow);
		
		myRow = new SparseRow(new TestElement(1));
		myRow.setElementValues( new int[]{ 4 , 5 , 3 } , new TestElement[]{
			new TestElement(0),
			new TestElement(3),
			new TestElement(2)
		});
		
		System.out.println("Creating another row: ");
		SparseRow myRow2 = new SparseRow(new TestElement(1));
		myRow2.setElementValues( new int[]{ 1 , 2 , 3 } , new TestElement[]{
			new TestElement(0),
			new TestElement(3),
			new TestElement(2)
		});
		System.out.print("Row A: " + myRow + "\nRow B : " + myRow2 +
				"\nSummedRows: " + myRow2.combineWith(myRow) + "\n");
		
		System.out.println();
		
		for(int i = 0; i < 10 ; i++){
			System.out.println("\tValue at column " + i + " is " + myRow2.getElementValueByColumn(i) + "  " +
			((!myRow2.getElementValueByColumn(i).isZeroValue())?"NOT NULL":"") +
			(myRow2.isDefaultValueEqualTo(myRow2.getElementValueByColumn(i))?"\t - IS DEFAULT VALUE FOR THIS ROW":"") );
		}
		
		System.out.println();
		System.out.println(myRow2.printProperties());
		myRow2.setElementValue(10, new TestElement(1000));
		System.out.println(myRow2.printRow());
		System.out.println(myRow2);
		System.out.println("Setting elements");
		System.out.println(   (myRow2.setElementValue(0, new TestElement(-1)))   );
		System.out.println("Sorting row \n" + myRow2);
		myRow2.bubbleSort();
		myRow2.setElementValue(12, new TestElement(15));
		myRow2.setElementValue(30, myRow2.getValueOfDefaultElement());
		System.out.println(myRow2.setElementValue(2000, myRow2.getValueOfDefaultElement()));
		
		myRow2 = new SparseRow(new TestElement(1));
		myRow2.setElementValues( new int[]{ 1 , 2 , 3 } , new TestElement[]{
			new TestElement(0),
			new TestElement(3),
			new TestElement(2)
		});
		
		
		System.out.println("non-default elements: " + myRow2.getNumberOfNonDefaultElements());
		System.out.println("Sorted row: " + myRow2.bubbleSort());
		SparseRow myRows [] = myRow2.getTranspose();
		System.out.println("Transposing myRow B.... the transp. has " + myRows.length + " rows, the length " + 
							"of myRow B is " + myRow2.getLargestColumnNumber());
		for(int i = 0 ; i < myRows.length ; i++) System.out.println(myRows[i]);
		
		int column = 0;
		
		System.out.println("myRow B * myRow B transp = " + myRow2.getSumOfMultiples(myRows,column));
		
		TestElement sum = new TestElement(0);
		for(int i = 0 ; i < myRows.length ; i++){
			TestElement temp = (TestElement)myRows[i].getElementValueByColumn(column).createNewInstance();
			System.out.println("Squaring " + temp);
			temp = (TestElement) temp.multiply(temp);
			sum = (TestElement) sum.add( temp );
		}
		System.out.println(sum);
		
		System.out.println("Row norm = " + myRow2.getRowNorm());
	}	
}